--[[ scheme_type: generic by: Alundaio --]] local AlwaysDetectDistance = nil local StateWalk = nil local StateRun = nil local MonsterLoot = nil local MonsterLootCommunities = nil lootable_table = nil local kind_blacklist = { -- prevent looting of these kinds ["o_light"] = true, ["o_medium"] = true, ["o_sci"] = true, ["o_heavy"] = true, ["o_helmet"] = true, } local HasValuableLoot = {} function has_valuable_loot(obj) return HasValuableLoot[obj:id()] == true end function set_valuable_loot(id,val) HasValuableLoot[id] = val end ------------------------------------------ -- Localized Functions ------------------------------------------ local function on_item_take(npc,item) if (npc:alive()) then return end -- remark npc as having valuables if actor places items on a corpse if (item and lootable_table[item:section()] ~= nil) then HasValuableLoot[npc:id()] = true return end end local function on_item_drop(npc,item) if (npc:alive()) then return end -- unmark npc as having valuables if they were all removed by actor local se_item = alife_object(item:id()) if (se_item and se_item.parent_id == AC_ID) then local corpse_contain_any_valuable HasValuableLoot[npc:id()] = nil local function check_item(npc,item) if (lootable_table[item:section()] ~= nil) then corpse_contain_any_valuable = true end end npc:iterate_inventory(check_item,npc) if (corpse_contain_any_valuable) then HasValuableLoot[npc:id()] = true end end end local function on_death_callback(npc,who) --[[ local corpse_contain_any_valuable HasValuableLoot[npc:id()] = nil local function check_item(npc,item) if (lootable_table[item:section()] ~= nil) then corpse_contain_any_valuable = true end end npc:iterate_inventory(check_item,npc) if (corpse_contain_any_valuable) then HasValuableLoot[npc:id()] = true end --]] -- block above commented since it's almost always certain stalker will have loot on death. TODO: Consider only non-storied npcs to be lootable HasValuableLoot[npc:id()] = true --save_var(npc,"death_by_id",who:id()) end local function monster_on_death_callback(obj,who) if not (obj) then return end if not (MonsterLoot) then return end HasValuableLoot[obj:id()] = nil local looted = se_load_var(obj:id(),obj:name(),"looted") == "true" if not looted then HasValuableLoot[obj:id()] = true end end local function on_pstor_load_all(obj) if not (obj and IsMonster(obj)) then return end if not (MonsterLoot) then return end HasValuableLoot[obj:id()] = nil if (obj:alive()) then return end local looted = se_load_var(obj:id(),obj:name(),"looted") == "true" if not looted then HasValuableLoot[obj:id()] = true end end ----------------------------------- -- Register Callbacks ----------------------------------- function on_game_start() local ini = ini_file("ai_tweaks\\xr_corpse_detection.ltx") AlwaysDetectDistance = ini:r_float_ex("settings","always_detect_dist") or 2500 StateWalk = ini:r_string_ex("settings","state_walk") or "walk_noweap" StateRun = ini:r_string_ex("settings","state_run") or "rush" MonsterLoot = ini:r_bool_ex("settings","enable_mutant_looting",false) MonsterLootCommunities = utils_data.collect_section(ini,"loot_mutant_communities",true) lootable_table = get_loot_table(ini) RegisterScriptCallback("npc_on_item_take",on_item_take) RegisterScriptCallback("npc_on_item_drop",on_item_drop) RegisterScriptCallback("npc_on_death_callback",on_death_callback) RegisterScriptCallback("monster_on_death_callback",monster_on_death_callback) RegisterScriptCallback("on_pstor_load_all",on_pstor_load_all) end function get_loot_table(ini) local t = {} ini_sys:section_for_each(function(section) if not (ini:line_exist("ignore_sections",section)) then if (ini_sys:r_bool_ex(section,"can_trade",true) == true) then if (ini_sys:r_bool_ex(section,"quest_item",false) == false) then if not (string.find(section,"mp_")) then local name = ini_sys:r_string_ex(section,"inv_name") if (name and name ~= "" and name ~= "default") then local cost = ini_sys:r_float_ex(section,"cost") or 0 if (cost > 0) then local kind = ini_sys:r_string_ex(section,"kind") if (not kind_blacklist[kind]) then t[section] = true end end end end end end end end ) return t end function get_all_from_corpse(npc) if not (db.actor) then return end local id = npc:id() local st = db.storage[id] and db.storage[id].corpse_detection -- sanity check, this should never happen if not (st) then return end local corpse_npc_id = st.selected_corpse_id local corpse_npc = corpse_npc_id and db.storage[corpse_npc_id] and db.storage[corpse_npc_id].object or corpse_npc_id and level.object_by_id(corpse_npc_id) -- reset all scheme dependent variables if (st.selected_corpse_id) then if (db.storage[st.selected_corpse_id]) then db.storage[st.selected_corpse_id].corpse_already_selected = nil end HasValuableLoot[st.selected_corpse_id] = nil end st.__stimer = nil st.mutant_analysed = nil st.vertex_id = nil st.vertex_position = nil st.selected_corpse_id = nil st.state = nil st.index = 1 st.nearest_corpse_dist = nil st.nearest_corpse_vertex = nil st.nearest_corpse_position = nil st.nearest_id = nil if (corpse_npc == nil or corpse_npc:alive() == true) then return end --printf("%s looting from %s",npc:name(),corpse_npc:name()) if not(IsStalker(corpse_npc)) then if (get_story_object("yan_ecolog_semenov")) then local need = load_var(db.actor,"yan_ecolog_semenov_task_1_tissue_need") or 0 if (need > 0) then local count = load_var(db.actor,"yan_ecolog_semenov_task_1_tissue_count") or 0 save_var(db.actor,"yan_ecolog_semenov_task_1_tissue_count",count + 1) end end local looted = se_load_var(corpse_npc:id(),corpse_npc:name(),"looted") == "true" if (not looted) and ui_mutant_loot then local loot = {} ui_mutant_loot.loot_mutant(corpse_npc:section(),corpse_npc:clsid(),loot,npc,nil,corpse_npc) local is_there_loot for sec,t in pairs(loot) do is_there_loot = true break end if (is_there_loot) then xr_sound.set_sound_play(id,"corpse_loot_good") else xr_sound.set_sound_play(id,"corpse_loot_bad") end se_save_var(corpse_npc:id(),corpse_npc:name(),"looted","true") game_statistics.increment_npc_statistic(npc,"field_dressings") end return end local items_value = 0 local sec local function get_item(corpse,item) SendScriptCallback("npc_on_get_all_from_corpse",npc,corpse_npc,item,lootable_table[item:section()]) if (lootable_table[item:section()] ~= nil) then if (items_value < 100) then items_value = items_value+item:cost()*item:condition()/30 end corpse:transfer_item(item,npc) end end corpse_npc:iterate_inventory(get_item,corpse_npc) if item_money then item_money.npc_on_loot_money(npc, corpse_npc) end if items_value >= 100 then xr_sound.set_sound_play(id,"corpse_loot_good") elseif items_value > 0 and (math.random(1,100)/100) < 0.5 then xr_sound.set_sound_play(id,"corpse_loot_bad") end local count = load_var(npc,"s_loot_count") or 0 if (count >= 255) then count = 254 end save_var(npc,"s_loot_count",count+1) game_statistics.increment_npc_statistic(npc,"corpse_looted") end ---------------------------------------------------------------------------------------------------------------------- -- EVALUATORS ---------------------------------------------------------------------------------------------------------------------- class "evaluator_corpse" (property_evaluator) function evaluator_corpse:__init(name, storage, npc) super (nil, name) self.a = storage end function near_actor(obj) local dist = tonumber(ui_options.get("gameplay/general/npc_loot_distance")) if dist and dist > 0 then return db.actor:position():distance_to(obj:position()) < dist else return false end end function evaluator_corpse:find_valid_target() if (self.a.cstackprevent) then printf("C Stack Overflow Prevention: warning xr_corpse_detection scheme making repeated calls without return; save now and reload!") return false end if (self.a.selected_corpse_id) then local corpse = db.storage[self.a.selected_corpse_id] and db.storage[self.a.selected_corpse_id].object or level.object_by_id(self.a.selected_corpse_id) if (corpse and HasValuableLoot[self.a.selected_corpse_id]) and not near_actor(corpse) then return true end if (db.storage[self.a.selected_corpse_id]) then db.storage[self.a.selected_corpse_id].corpse_already_selected = nil end end local tg = time_global() self.a.__dtimer = not self.a.__dtimer and tg + 250 or self.a.__dtimer if (tg < self.a.__dtimer) then return false end self.a.__dtimer = nil if not (self.a.index) then self.a.index = 1 end if not (self.a.memory) then self.a.memory = {} end local npc = self.object local size = #self.a.memory if (size == 0) then self.a.cstackprevent = true for o in npc:memory_visible_objects() do local obj = o and o:object() if (obj and (IsStalker(obj) or IsMonster(obj)) and obj:alive() ~= true and not near_actor(obj)) then size = size + 1 self.a.memory[size] = obj:id() end end self.a.cstackprevent = nil end if (size == 0 or self.a.index > size) then if (self.a.nearest_id and HasValuableLoot[self.a.nearest_id] and db.storage[self.a.nearest_id] and db.storage[self.a.nearest_id].corpse_already_selected == nil) then if (self.a.selected_corpse_id and db.storage[self.a.selected_corpse_id]) then db.storage[self.a.selected_corpse_id].corpse_already_selected = nil end self.a.vertex_id = self.a.nearest_corpse_vertex self.a.vertex_position = self.a.nearest_corpse_position self.a.selected_corpse_id = self.a.nearest_id self.a.state = self.a.nearest_state db.storage[self.a.selected_corpse_id].corpse_already_selected = npc:id() self.a.index = 1 self.a.nearest_corpse_dist = nil self.a.nearest_corpse_vertex = nil self.a.nearest_corpse_position = nil self.a.nearest_id = nil return true end self.a.index = 1 self.a.nearest_corpse_dist = nil self.a.nearest_corpse_vertex = nil self.a.nearest_corpse_position = nil self.a.nearest_id = nil empty_table(self.a.memory) return false end local id = self.a.memory[self.a.index] local corpse_npc = id and db.storage[id] and db.storage[id].object if (corpse_npc and corpse_npc:alive() ~= true and HasValuableLoot[id] and db.storage[id] and db.storage[id].corpse_already_selected == nil) then local is_stalker = IsStalker(corpse_npc) local can_loot_mutants = not is_stalker and (self.a.analse_mode == "true" or self.a.enabled == "true" and MonsterLootCommunities[character_community(npc)]) if (self.a.enabled == "true" and is_stalker or can_loot_mutants) then local corpse_pos = utils_obj.safe_bone_pos(corpse_npc,"bip01_spine") local dist = npc:position():distance_to_sqr(corpse_pos) if (dist < AlwaysDetectDistance) and (self.a.nearest_corpse_dist == nil or dist <= self.a.nearest_corpse_dist) then local corpse_vertex = level.vertex_id(corpse_pos) if level.vertex_position(corpse_vertex):distance_to_sqr(corpse_pos) > 16 then corpse_vertex = corpse_npc:level_vertex_id() end if (npc:accessible(corpse_vertex) and level.vertex_position(corpse_vertex):distance_to_sqr(corpse_pos) <= 15) then self.a.nearest_corpse_dist = dist self.a.nearest_corpse_vertex = corpse_vertex self.a.nearest_corpse_position = corpse_pos self.a.nearest_id = id self.a.nearest_state = dist < math.random(5,30) and StateWalk or StateRun end end end end self.a.index = self.a.index + 1 return false end function evaluator_corpse:evaluate() --utils_data.debug_write("eva_corpse") if not (self.object:alive()) then return false end if (xr_danger.has_danger(self.object)) then return false end if (xr_conditions.surge_started() == true) then return false end local npc = self.object if (IsWounded(npc) or npc:best_enemy())then return false end if utils_item.is_overweight(npc) then return false end local st = db.storage[npc:id()] if (st) and ((st.active_scheme == "camper") or (st.help_wounded and st.help_wounded.selected_id ~= nil) or (st.gather_items and st.gather_items.selected_id ~= nil)) then return false end self.a.analse_mode = xr_logic.pick_section_from_condlist(db.actor, self.object, self.a.mutant_corpse_analysis) self.a.enabled = xr_logic.pick_section_from_condlist(db.actor, self.object, self.a.corpse_detection_enabled) if (self.a.analse_mode == "false" and self.a.enabled == "false") then return false end if (self:find_valid_target()) then return true end return false end ---------------------------------------------------------------------------------------------------------------------- --Actions ---------------------------------------------------------------------------------------------------------------------- class "action_search_corpse" (action_base) function action_search_corpse:__init (npc_name,action_name,storage) super (nil,action_name) self.a = storage end function action_search_corpse:initialize() action_base.initialize(self) local npc = self.object state_mgr.set_state(npc,self.a.state) npc:set_desired_position() npc:set_desired_direction() npc:set_path_type(game_object.level_path) self.a.vertex_id = utils_obj.send_to_nearest_accessible_vertex(npc,self.a.vertex_id,"corpse_detection") end function action_search_corpse:execute() action_base.execute(self) local npc = self.object local current_state = state_mgr.get_state(npc) if (current_state == "search_corpse" or current_state == "field_dress") then local tg = time_global() self.a.__stimer = not self.a.__stimer and tg + 15000 or self.a.__stimer if (tg > self.a.__stimer) then self.a.__stimer = nil get_all_from_corpse(npc) end return elseif (current_state == "scaner_crouch" and not self.a.mutant_analysed) then local tg = time_global() self.a.__stimer = not self.a.__stimer and tg + 12000 or self.a.__stimer if (tg < self.a.__stimer) then return end self.a.mutant_analysed = true self.a.__stimer = nil end if not (self.a.selected_corpse_id) then return end local tg = time_global() if (npc:level_vertex_id() == self.a.vertex_id) then if not self.sound then if (math.random(1,100)/100) < 0.60 then xr_sound.set_sound_play(npc:id(),"corpse_loot_begin") end self.sound = true end local corpse = db.storage[self.a.selected_corpse_id] and db.storage[self.a.selected_corpse_id].object or level.object_by_id(self.a.selected_corpse_id) look_pos = corpse and utils_obj.safe_bone_pos(corpse,"bip01_spine") or self.a.vertex_position if (corpse) then if (IsMonster(corpse)) then state_mgr.set_state(npc,"field_dress",nil,nil,{look_position = look_pos}) return end -- moved from xr_motivator to here and xr_motivator use_callback death_manager.create_release_item(corpse) end state_mgr.set_state(npc,"search_corpse",nil,nil,{look_position = look_pos}) else if (self.a.analse_mode == "true" and not self.a.mutant_analysed) then local dist = self.object:position():distance_to_sqr(self.a.vertex_position) if (dist <= 6.25) then state_mgr.set_state(npc,"scaner_crouch") return end end if (npc:path_type() ~= game_object.level_path) then npc:set_path_type(game_object.level_path) end state_mgr.set_state(npc,self.a.state or "patrol") self.a.vertex_id = utils_obj.send_to_nearest_accessible_vertex(npc, self.a.vertex_id,"corpse_detection") end end function action_search_corpse:finalize() --state_mgr.set_state(self.object, "idle") local st = self.a if (st.selected_corpse_id and db.storage[st.selected_corpse_id]) then db.storage[st.selected_corpse_id].corpse_already_selected = nil end st.__stimer = nil st.mutant_analysed = nil st.vertex_id = nil st.vertex_position = nil st.selected_corpse_id = nil st.state = nil st.index = 1 st.nearest_corpse_dist = nil st.nearest_corpse_vertex = nil st.nearest_corpse_position = nil st.nearest_id = nil action_base.finalize(self) end --------------------------------------------------------------------------------------------------------------------- -- BINDER ---------------------------------------------------------------------------------------------------------------------- function setup_generic_scheme(npc,ini,scheme,section,stype,temp) local st = xr_logic.assign_storage_and_bind(npc,ini,"corpse_detection",section,temp) end function add_to_binder(npc,char_ini,scheme,section,st,temp) if not npc then return end local manager = npc:motivation_action_manager() if not manager then return end if (character_community(npc) == "zombied" or npc:section() == "actor_visual_stalker") then manager:add_evaluator(xr_evaluators_id.corpse_exist,property_evaluator_const(false)) temp.needs_configured = false return end local evaluator = evaluator_corpse("corpse_exist", st) temp.action = action_search_corpse(npc:name(),"action_search_corpse", st) if not evaluator or not temp.action then return end manager:add_evaluator(xr_evaluators_id.corpse_exist, evaluator) temp.action:add_precondition(world_property(stalker_ids.property_alive,true)) temp.action:add_precondition(world_property(stalker_ids.property_enemy,false)) temp.action:add_precondition(world_property(stalker_ids.property_danger,false)) temp.action:add_precondition(world_property(xr_evaluators_id.corpse_exist, true)) temp.action:add_effect(world_property(xr_evaluators_id.corpse_exist, false)) manager:add_action(xr_actions_id.corpse_exist,temp.action) end function configure_actions(npc,ini,scheme,section,stype,temp) if not npc then return end local manager = npc:motivation_action_manager() if not manager or not temp.action then return end temp.action:add_precondition(world_property(xr_gather_items.evaid,false)) temp.action:add_precondition(world_property(stalker_ids.property_items,false)) --temp.action:add_precondition(world_property(xr_evaluators_id.wounded_exist,false)) temp.action:add_precondition(world_property(xr_evaluators_id.stohe_meet_base + 1,false)) temp.action:add_precondition(world_property(xr_evaluators_id.sidor_wounded_base,false)) temp.action:add_precondition(world_property(xr_evaluators_id.abuse_base, false)) --temp.action:add_precondition(world_property(axr_fight_from_cover.evaid, false)) --temp.action:add_precondition(world_property(axr_stalker_panic.evaid, false)) --temp.action:add_precondition(world_property(axr_npc_vs_heli.evaid, false)) --temp.action:add_precondition(world_property(xr_evaluators_id.state_mgr + 1,false)) local action local p = { xr_actions_id.state_mgr + 2, xr_actions_id.alife } for i=1,#p do --printf("ACTION_ALIFE_ID(xr_corpse_detection.configure_actions): " .. tostring(p[i])) action = manager:action(p[i]) if (action) then action:add_precondition( world_property(xr_evaluators_id.corpse_exist,false) ) else printe("!ERROR: xr_corpse_detection: no action id p[%s]",i) end end end function reset_generic_scheme(npc,scheme,section,stype,st) if not (st) then printe("!ERROR: xr_corpse_detection: reset_generic_scheme storage is nil! npc=%s scheme=%s section=%s",npc and npc:name(),scheme,section) return end --printf("TEST: scheme=%s st[scheme]=%s",scheme,st[scheme] ~= nil) if (st.corpse_detection.selected_corpse_id) then if (db.storage[st.corpse_detection.selected_corpse_id]) then db.storage[st.corpse_detection.selected_corpse_id].corpse_already_selected = nil end st.corpse_detection.selected_corpse_id = nil end st.corpse_detection.vertex_id = nil st.corpse_detection.vertex_position = nil st.corpse_detection.mutant_corpse_analysis = xr_logic.parse_condlist(npc, section, "mutant_corpse_analysis", st.ini:r_string_ex(section,"mutant_corpse_analysis") or "false") st.corpse_detection.corpse_detection_enabled = xr_logic.parse_condlist(npc, section, "corpse_detection_enabled",st.ini:r_string_ex(section,"corpse_detection_enabled") or "true") end function is_under_corpse_detection(npc) local mgr = npc:motivation_action_manager() if not mgr or not mgr:initialized() then return false end local current_action_id = mgr:current_action_id() return current_action_id == xr_actions_id.corpse_exist end